Presentation Model
例1
Martin Fowler 氏のページでの例を見てみる。
まず、Domain Model は単純にデータを返すだけのクラスになっている。
code:AlbumDataSet.cpp
public static DsAlbum AlbumDataSet() {
DsAlbum result = new DsAlbum();
result.Albums.AddAlbumsRow(1, "HQ", "Roy Harper", false, null);
result.Albums.AddAlbumsRow(2, "The Rough Dancer and Cyclical Night", "Astor Piazzola", false, null);
result.Albums.AddAlbumsRow(3, "The Black Light", "Calexico", false, null);
result.Albums.AddAlbumsRow(4, "Symphony No.5", "CBSO", true, "Sibelius" );
result.AcceptChanges();
return result;
}
code:PmodAlbum.cpp
// Data Model を保持する
private DsAlbum _data;
// View 固有の状態も保持する
// 今回の場合は、現在 View 上で選択されているアルバムの番号
private int _selectedAlbumNumber;
public PmodAlbum(DsAlbum albums) {
this._data = albums;
_selectedAlbumNumber = 0;
}
// Data Model 内のデータは、Data Model から Pull したものをそのまま View に受け渡す
public String Title {
get {return SelectedAlbum.Title;}
set {SelectedAlbum.Title = value;}
}
public String Artist {
get {return SelectedAlbum.Artist;}
set {SelectedAlbum.Artist = value;}
}
public bool IsClassical {
get {return SelectedAlbum.IsClassical;}
set {SelectedAlbum.IsClassical = value;}
}
public String Composer {
get {
return (SelectedAlbum.IsComposerNull()) ? "" : SelectedAlbum.Composer;
}
set {
if (IsClassical) SelectedAlbum.Composer = value;
}
}
// View 固有の状態も、Data Model 内の他のデータと同様に受け渡せる
public DsAlbum.AlbumsRow SelectedAlbum {
}}
// 他の状態に依存した新たなプロパティも定義できる
public String FormTitle
{
get {return "Album: " + Title;}
}
// View 上の ComposerField が enabled/disabled のどちらか?という View 固有の状態を保持する
// IsClassical を直接は返さない
// Presentation Model は、どのようなロジックが Composer Field の有効/無効に反映されるか?を決定する
// 「IsClassicalの値がそのままComposerFieldの有効/無効」という事実は Presentation Model のみが知っているべき
public bool IsComposerFieldEnabled {
get {return IsClassical;}
}
// 他の View 固有の状態でも同様の考え方でカプセル化していく
public bool IsApplyEnabled {
get {return HasRowChanged;}
}
public bool IsCancelEnabled {
get {return HasRowChanged;}
}
public bool HasRowChanged {
get {return SelectedAlbum.RowState == DataRowState.Modified;}
}
// View 上にリストがある場合は、そのリストに表示するアイテム一覧、のようなものも、Presentation Model が保持する
public String[] AlbumList {
get {
for (int i = 0; i < result.Length; i++)
resulti = Data.Albumsi.Title; return result;
}
}
Presentation Model に対し、View も定義する。View では、さらに Presentation Model と同期するためのメソッドを用意する。
code:FrmAlbum.cpp
// View -> Presentation Model への同期
// model への参照は View 自体が保持している
private void SaveToPmod() {
model.Artist = txtArtist.Text;
model.Title = txtTitle.Text;
model.IsClassical = chkClassical.Checked;
model.Composer = txtComposer.Text;
}
// Presentation Model -> View への同期
// この同期は GUIコンポーネント 上で更新があった場合にトリガーされるが、
// このメソッド自体も GUIコンポーネント 上に更新を発生させるので、無限ループに陥ってしまう
// これを防ぐためにガード条件を設けている
private void LoadFromPmod() {
if (NotLoadingView) {
_isLoadingView = true;
lstAlbums.DataSource = model.AlbumList;
lstAlbums.SelectedIndex = model.SelectedAlbumNumber;
txtArtist.Text = model.Artist;
txtTitle.Text = model.Title;
this.Text = model.FormTitle;
chkClassical.Checked = model.IsClassical;
txtComposer.Enabled = model.IsComposerFieldEnabled;
txtComposer.Text = model.Composer;
btnApply.Enabled = model.IsApplyEnabled;
btnCancel.Enabled = model.IsCancelEnabled;
_isLoadingView = false;
}
}
private bool _isLoadingView = false;
private bool NotLoadingView {
get {return !_isLoadingView;}
}
// 双方向の同期用に呼び出すメソッド
private void SyncWithPmod() {
if (NotLoadingView) {
SaveToPmod();
LoadFromPmod();
}
}
// フィールドに更新があった場合に呼び出されるハンドラーに、同期用のメソッドを仕込んでおく
private void txtTitle_TextChanged(object sender, System.EventArgs e){
SyncWithPmod();
}
// アイテムが選択されたら、Presentation Model を更新してロードし直す、といったことができる
private void lstAlbums_SelectedIndexChanged(object sender, System.EventArgs e){
if (NotLoadingView) {
model.SelectedAlbumNumber = lstAlbums.SelectedIndex;
LoadFromPmod();
}
}
// データの同期だけでなく、Presentation Model に複雑な振る舞いを委譲する
private void btnApply_Click(object sender, System.EventArgs e) {
model.Apply();
LoadFromPmod();
}
private void btnCancel_Click(object sender, System.EventArgs e){
model.Cancel();
LoadFromPmod();
}
例2
先ほどと同じように Data Model を作っておく。
code:AlbumList.cpp
public static AlbumList AlbumGridDataSet()
{
AlbumList result = new AlbumList();
result.Albums.AddAlbumsRow(1, "HQ", "Roy Harper", "Rock");
result.Albums.AddAlbumsRow(2, "Lemonade and Buns", "Kila", "Celtic");
result.Albums.AddAlbumsRow(3, "Stormcock", "Roy Harper", "Rock");
result.Albums.AddAlbumsRow(4, "Zero Hour", "Astor Piazzola", "Tango");
result.Albums.AddAlbumsRow(5, "The Rough Dancer and Cyclical Night", "Astor Piazzola", "Tango");
result.Albums.AddAlbumsRow(6, "The Black Light", "Calexico", "Rock");
result.Albums.AddAlbumsRow(7, "Spoke", "Calexico", "Rock");
result.Albums.AddAlbumsRow(8, "Electrica", "Daniela Mercury", "Brazil");
result.Albums.AddAlbumsRow(9, "Feijao com Arroz", "Daniela Mercury", "Brazil");
result.Albums.AddAlbumsRow(10, "Sol da Libertade", "Daniela Mercury", "Brazil");
Console.WriteLine(result);
return result;
}
code:.cpp
private AlbumList _dsAlbums;
internal AlbumList DsAlbums {
get {return _dsAlbums;}
}
// 特定の行の色を返す
// その位置の行が条件にマッチしていたら、色を変更する
internal Color RowColor(int row) {
return (Albumsrow.genre.Equals("Rock")) ? Color.Cornsilk : Color.White; }
private AlbumList.AlbumsDataTable Albums {
get {return DsAlbums.Albums;}
}
色を調整したい場合、WinForms は行ごとに色を設定する機能がないため、達成するのに少し手間がかかる。これを達成するには、DataGridTextBoxColumn をサブクラス化し、色を取得するためのハンドラーをうけたわす。この時、オリジナルの DataGridTextBoxColumn も渡す。
code:ColorableDataGridTextBoxColumn.cpp
public ColorableDataGridTextBoxColumn (ColorGetter getcolorRowCol, DataGridTextBoxColumn original)
{
_delGetColor = getcolorRowCol;
copyFrom(original);
}
public delegate Color ColorGetter(int row);
private ColorGetter _delGetColor;
// これは Decorater パターンを実現しようとしているのだが、WinForms のオブジェクトはかなり守られている
// そのため、代わりに全プロパティをコピーする
// read すらできないプロパティもあるためうまくいかないが、今の所うまく動く
void copyFrom (DataGridTextBoxColumn original) {
PropertyInfo[] props = original.GetType().GetProperties();
foreach (PropertyInfo p in props) {
if (p.CanWrite && p.CanRead)
p.SetValue(this, p.GetValue(original, null), null) ;
}
}
// 適切な色を取得して色をペイントさせる
protected override void Paint(System.Drawing.Graphics g, System.Drawing.Rectangle bounds,
System.Windows.Forms.CurrencyManager source, int rowNum,
System.Drawing.Brush backBrush, System.Drawing.Brush foreBrush,
bool alignToRight)
{
base.Paint(g, bounds, source, rowNum, new SolidBrush(_delGetColor(rowNum)), foreBrush, alignToRight);
}
public static void ReplaceColumnStyles(DataGridTableStyle grid, ColorGetter del) {
for (int i = 0; i < grid.GridColumnStyles.Count; i++) {
DataGridTextBoxColumn old = (DataGridTextBoxColumn) grid.GridColumnStyles0; grid.GridColumnStyles.RemoveAt(0);
grid.GridColumnStyles.Add(new ColorableDataGridTextBoxColumn(del, old));
}
}
code:FrmAlbums.cpp
// ページロード時に、カラムのスタイルを設定させる
private void FrmAlbums_Load(object sender, System.EventArgs e){
bindData();
replaceColumnStyles();
}
private void replaceColumnStyles() {
ColorableDataGridTextBoxColumn.ReplaceColumnStyles(dgsAlbums,
new ColorableDataGridTextBoxColumn.ColorGetter(model.RowColor));
}
Visual Studio の Data Binding の機能によって、テーブルセル <-> DataSet, Presentation Moedl 間の Data Binding を設定できる。